/*
 *              Copyright (C) 2011 The MusicMod Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *            http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.musicmod.android.view;

import org.musicmod.android.R;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

public class VerticalTextSpinner extends View {

	private static final int SELECTOR_ARROW_HEIGHT = 15;

	private static int TEXT_SPACING;
	private static int TEXT_MARGIN_RIGHT;
	private static int TEXT_SIZE;
	private static int TEXT1_Y;
	private static int TEXT2_Y;
	private static int TEXT3_Y;
	private static int TEXT4_Y;
	private static int TEXT5_Y;
	private static int SCROLL_DISTANCE;

	private static final int SCROLL_MODE_NONE = 0;
	private static final int SCROLL_MODE_UP = 1;
	private static final int SCROLL_MODE_DOWN = 2;

	private static final long DEFAULT_SCROLL_INTERVAL_MS = 400;
	private static final int MIN_ANIMATIONS = 4;

	private final Drawable mBackgroundFocused;
	private final Drawable mSelectorFocused;
	private final Drawable mSelectorNormal;
	private final int mSelectorDefaultY;
	private final int mSelectorMinY;
	private final int mSelectorMaxY;
	private final int mSelectorHeight;
	private final TextPaint mTextPaintDark;
	private final TextPaint mTextPaintLight;

	private int mSelectorY;
	private Drawable mSelector;
	private int mDownY;
	private boolean isDraggingSelector;
	private int mScrollMode;
	private long mScrollInterval;
	private boolean mIsAnimationRunning;
	private boolean mStopAnimation;
	private boolean mWrapAround = true;

	private int mTotalAnimatedDistance;
	private int mNumberOfAnimations;
	private long mDelayBetweenAnimations;
	private int mDistanceOfEachAnimation;

	private String[] mTextList;
	private int mCurrentSelectedPos;
	private OnChangedListener mListener;

	private String mText1;
	private String mText2;
	private String mText3;
	private String mText4;
	private String mText5;

	public interface OnChangedListener {
		void onChanged(VerticalTextSpinner spinner, int oldPos, int newPos,
				String[] items);
	}

	public VerticalTextSpinner(Context context) {
		this(context, null);
	}

	public VerticalTextSpinner(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public VerticalTextSpinner(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);

		float scale = getResources().getDisplayMetrics().density;
		TEXT_SPACING = (int) (18 * scale);
		TEXT_MARGIN_RIGHT = (int) (25 * scale);
		TEXT_SIZE = (int) (22 * scale);
		SCROLL_DISTANCE = TEXT_SIZE + TEXT_SPACING;
		TEXT1_Y = (TEXT_SIZE * (-2 + 2)) + (TEXT_SPACING * (-2 + 1));
		TEXT2_Y = (TEXT_SIZE * (-1 + 2)) + (TEXT_SPACING * (-1 + 1));
		TEXT3_Y = (TEXT_SIZE * (0 + 2)) + (TEXT_SPACING * (0 + 1));
		TEXT4_Y = (TEXT_SIZE * (1 + 2)) + (TEXT_SPACING * (1 + 1));
		TEXT5_Y = (TEXT_SIZE * (2 + 2)) + (TEXT_SPACING * (2 + 1));

		mBackgroundFocused = context.getResources().getDrawable(
				R.drawable.pickerbox_background);
		mSelectorFocused = context.getResources().getDrawable(
				R.drawable.pickerbox_selected);
		mSelectorNormal = context.getResources().getDrawable(
				R.drawable.pickerbox_unselected);

		mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
		mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
		mSelectorMinY = 0;
		mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight()
				- mSelectorHeight;

		mSelector = mSelectorNormal;
		mSelectorY = mSelectorDefaultY;

		mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
		mTextPaintDark.setTextSize(TEXT_SIZE);
		mTextPaintDark.setColor(context.getResources().getColor(
				android.R.color.primary_text_light));

		mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
		mTextPaintLight.setTextSize(TEXT_SIZE);
		mTextPaintLight.setColor(context.getResources().getColor(
				android.R.color.secondary_text_dark));

		mScrollMode = SCROLL_MODE_NONE;
		mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
		calculateAnimationValues();
	}

	public void setOnChangeListener(OnChangedListener listener) {
		mListener = listener;
	}

	public void setItems(String[] textList) {
		mTextList = textList;
		calculateTextPositions();
	}

	public void setSelectedPos(int selectedPos) {
		mCurrentSelectedPos = selectedPos;
		calculateTextPositions();
		postInvalidate();
	}

	public void setScrollInterval(long interval) {
		mScrollInterval = interval;
		calculateAnimationValues();
	}

	public void setWrapAround(boolean wrap) {
		mWrapAround = wrap;
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {

		/*
		 * This is a bit confusing, when we get the key event DPAD_DOWN we
		 * actually roll the spinner up. When the key event is DPAD_UP we roll
		 * the spinner down.
		 */
		if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) {
			mScrollMode = SCROLL_MODE_DOWN;
			scroll();
			mStopAnimation = true;
			return true;
		} else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) {
			mScrollMode = SCROLL_MODE_UP;
			scroll();
			mStopAnimation = true;
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}

	private boolean canScrollDown() {
		return (mCurrentSelectedPos > 0) || mWrapAround;
	}

	private boolean canScrollUp() {
		return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
	}

	@Override
	protected void onFocusChanged(boolean gainFocus, int direction,
			Rect previouslyFocusedRect) {
		if (gainFocus) {
			setBackgroundDrawable(mBackgroundFocused);
			mSelector = mSelectorFocused;
		} else {
			setBackgroundDrawable(null);
			mSelector = mSelectorNormal;
			mSelectorY = mSelectorDefaultY;
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		final int action = event.getAction();
		final int y = (int) event.getY();

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			requestFocus();
			mDownY = y;
			isDraggingSelector = (y >= mSelectorY)
					&& (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
			break;

		case MotionEvent.ACTION_MOVE:
			if (isDraggingSelector) {
				int top = mSelectorDefaultY + (y - mDownY);
				if (top <= mSelectorMinY && canScrollDown()) {
					mSelectorY = mSelectorMinY;
					mStopAnimation = false;
					if (mScrollMode != SCROLL_MODE_DOWN) {
						mScrollMode = SCROLL_MODE_DOWN;
						scroll();
					}
				} else if (top >= mSelectorMaxY && canScrollUp()) {
					mSelectorY = mSelectorMaxY;
					mStopAnimation = false;
					if (mScrollMode != SCROLL_MODE_UP) {
						mScrollMode = SCROLL_MODE_UP;
						scroll();
					}
				} else {
					mSelectorY = top;
					mStopAnimation = true;
				}
			}
			break;

		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
		default:
			mSelectorY = mSelectorDefaultY;
			mStopAnimation = true;
			invalidate();
			break;
		}
		return true;
	}

	@Override
	protected void onDraw(Canvas canvas) {

		/* The bounds of the selector */
		final int selectorLeft = 0;
		final int selectorTop = mSelectorY;
		final int selectorRight = getWidth();
		final int selectorBottom = mSelectorY + mSelectorHeight;

		/* Draw the selector */
		mSelector.setBounds(selectorLeft, selectorTop, selectorRight,
				selectorBottom);
		mSelector.draw(canvas);

		if (mTextList == null) {

			/* We're not setup with values so don't draw anything else */
			return;
		}

		final TextPaint textPaintDark = mTextPaintDark;
		if (hasFocus()) {

			/* The bounds of the top area where the text should be light */
			final int topLeft = 0;
			final int topTop = 0;
			final int topRight = selectorRight;
			final int topBottom = selectorTop + SELECTOR_ARROW_HEIGHT;

			/* Assign a bunch of local finals for performance */
			final String text1 = mText1;
			final String text2 = mText2;
			final String text3 = mText3;
			final String text4 = mText4;
			final String text5 = mText5;
			final TextPaint textPaintLight = mTextPaintLight;

			/*
			 * Draw the 1st, 2nd and 3rd item in light only, clip it so it only
			 * draws in the area above the selector
			 */
			canvas.save();
			canvas.clipRect(topLeft, topTop, topRight, topBottom);
			drawText(canvas, text1, TEXT1_Y + mTotalAnimatedDistance,
					textPaintLight);
			drawText(canvas, text2, TEXT2_Y + mTotalAnimatedDistance,
					textPaintLight);
			drawText(canvas, text3, TEXT3_Y + mTotalAnimatedDistance,
					textPaintLight);
			canvas.restore();

			/*
			 * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark
			 * paint
			 */
			canvas.save();
			canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT,
					selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT);
			drawText(canvas, text2, TEXT2_Y + mTotalAnimatedDistance,
					textPaintDark);
			drawText(canvas, text3, TEXT3_Y + mTotalAnimatedDistance,
					textPaintDark);
			drawText(canvas, text4, TEXT4_Y + mTotalAnimatedDistance,
					textPaintDark);
			canvas.restore();

			/* The bounds of the bottom area where the text should be light */
			final int bottomLeft = 0;
			final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT;
			final int bottomRight = selectorRight;
			final int bottomBottom = getMeasuredHeight();

			/*
			 * Draw the 3rd, 4th and 5th in white text, clip it so it only draws
			 * in the area below the selector.
			 */
			canvas.save();
			canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom);
			drawText(canvas, text3, TEXT3_Y + mTotalAnimatedDistance,
					textPaintLight);
			drawText(canvas, text4, TEXT4_Y + mTotalAnimatedDistance,
					textPaintLight);
			drawText(canvas, text5, TEXT5_Y + mTotalAnimatedDistance,
					textPaintLight);
			canvas.restore();

		} else {
			drawText(canvas, mText3, TEXT3_Y, textPaintDark);
		}
		if (mIsAnimationRunning) {
			if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) {
				mTotalAnimatedDistance = 0;
				if (mScrollMode == SCROLL_MODE_UP) {
					int oldPos = mCurrentSelectedPos;
					int newPos = getNewIndex(1);
					if (newPos >= 0) {
						mCurrentSelectedPos = newPos;
						if (mListener != null) {
							mListener.onChanged(this, oldPos,
									mCurrentSelectedPos, mTextList);
						}
					}
					if (newPos < 0
							|| ((newPos >= mTextList.length - 1) && !mWrapAround)) {
						mStopAnimation = true;
					}
					calculateTextPositions();
				} else if (mScrollMode == SCROLL_MODE_DOWN) {
					int oldPos = mCurrentSelectedPos;
					int newPos = getNewIndex(-1);
					if (newPos >= 0) {
						mCurrentSelectedPos = newPos;
						if (mListener != null) {
							mListener.onChanged(this, oldPos,
									mCurrentSelectedPos, mTextList);
						}
					}
					if (newPos < 0 || (newPos == 0 && !mWrapAround)) {
						mStopAnimation = true;
					}
					calculateTextPositions();
				}
				if (mStopAnimation) {
					final int previousScrollMode = mScrollMode;

					/*
					 * No longer scrolling, we wait till the current animation
					 * completes then we stop.
					 */
					mIsAnimationRunning = false;
					mStopAnimation = false;
					mScrollMode = SCROLL_MODE_NONE;

					/*
					 * If the current selected item is an empty string scroll
					 * past it.
					 */
					if ("".equals(mTextList[mCurrentSelectedPos])) {
						mScrollMode = previousScrollMode;
						scroll();
						mStopAnimation = true;
					}
				}
			} else {
				if (mScrollMode == SCROLL_MODE_UP) {
					mTotalAnimatedDistance -= mDistanceOfEachAnimation;
				} else if (mScrollMode == SCROLL_MODE_DOWN) {
					mTotalAnimatedDistance += mDistanceOfEachAnimation;
				}
			}
			if (mDelayBetweenAnimations > 0) {
				postInvalidateDelayed(mDelayBetweenAnimations);
			} else {
				invalidate();
			}
		}
	}

	/**
	 * Called every time the text items or current position changes. We
	 * calculate store we don't have to calculate onDraw.
	 */
	private void calculateTextPositions() {
		mText1 = getTextToDraw(-2);
		mText2 = getTextToDraw(-1);
		mText3 = getTextToDraw(0);
		mText4 = getTextToDraw(1);
		mText5 = getTextToDraw(2);
	}

	private String getTextToDraw(int offset) {
		int index = getNewIndex(offset);
		if (index < 0) {
			return "";
		}
		return mTextList[index];
	}

	private int getNewIndex(int offset) {
		int index = mCurrentSelectedPos + offset;
		if (index < 0) {
			if (mWrapAround) {
				index += mTextList.length;
			} else {
				return -1;
			}
		} else if (index >= mTextList.length) {
			if (mWrapAround) {
				index -= mTextList.length;
			} else {
				return -1;
			}
		}
		return index;
	}

	private void scroll() {
		if (mIsAnimationRunning) {
			return;
		}
		mTotalAnimatedDistance = 0;
		mIsAnimationRunning = true;
		invalidate();
	}

	private void calculateAnimationValues() {
		mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE;
		if (mNumberOfAnimations < MIN_ANIMATIONS) {
			mNumberOfAnimations = MIN_ANIMATIONS;
			mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
			mDelayBetweenAnimations = 0;
		} else {
			mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
			mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations;
		}
	}

	private void drawText(Canvas canvas, String text, int y, TextPaint paint) {
		int width = (int) paint.measureText(text);
		int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT;
		canvas.drawText(text, x, y, paint);
	}

	public int getCurrentSelectedPos() {
		return mCurrentSelectedPos;
	}
}
